<!DOCTYPE html>
<html>
<head>
<title>MuPDF / WebAssembly</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<style>

:root {
	--menubar-height: 22pt;
	--sidebar-width: 250px;
}

html, body { margin: 0; padding: 0; }

html { background-color: gray; }

input, button { border:none; background-image:none; background-color:transparent; box-shadow:none; margin:0; padding:0; }
input { border:2px solid #777; background-color: white; height: 2em; padding-left:1ex; }
button { border:2px solid #555; background-color: wheat; min-width: 2em; height: 2em; padding: 0 1ex; }
button:hover { background-color: orange; }

/* MENUS */

#grid-menubar { font-family: "Segoe UI", "Arial", "sans-serif"; }

#grid-menubar {
	position: fixed;
	z-index: 3;
	top: 0;
	left: 0;
	font-size: 12pt;
	line-height: 1;
	height: var(--menubar-height);
	width: 100%;
	background-color: lightsteelblue;
	user-select: none;
	box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.1);
}

.menu { float: left; }

.menu-button { padding: 5pt 10pt; }

.menu-popup {
	display: none;
	position: absolute;
	background-color: white;
	min-width: 20ex;
	white-space: nowrap;
	box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
	z-index: 3;
}

.menu-popup hr { margin: 0; border-bottom: 0; border-top: 1px solid gray; }
.menu-popup div { padding: 5pt 10pt; }
.menu-popup div:hover { background-color: steelblue; color: white; }

.menu:hover .menu-popup { display: block; }
.menu:hover .menu-button { background-color: steelblue; color: white; }

/* SIDEBAR */

#grid-sidebar { font-family: "Times New Roman", "serif"; }

#grid-sidebar {
	display: none;
	position: fixed;
	z-index: 1;
	top: var(--menubar-height);
	left: 0;
	width: var(--sidebar-width);
	height: calc(100% - var(--menubar-height));
	background-color: white;
	overflow: auto;
}

#outline { margin:0; padding:1ex; padding-left:2ex; list-style-type: none; font-size: 10pt; }
#outline ul { margin:0; padding:0; padding-left:3ex; list-style-type: none; }
#outline a { text-decoration:none; color: black; }
#outline a:hover { text-decoration:underline; }

/* DIALOGS */

div.dialog {
	position:fixed;
	top:22pt;
	left:0;
	right:0;
	margin-left: auto;
	NO-margin-right: auto;
	width:max-content;
	background-color:thistle;
	padding:1em;
	z-index:2;
	box-shadow: 0px 4px 16px 0px rgba(0,0,0,0.2);
}

#searchStatus {
	padding-top: 1ex;
}

/* PAGES */

#grid-pages { padding-top: var(--menubar-height); }
#grid-pages.sidebarVisible { padding-left: var(--sidebar-width); }
#grid-pages.sidebarHidden { padding-left: 0; }

#pages { margin: 0 auto; }

a.anchor {
	display:block;
	position:relative;
	top:-22pt;
	visibility:hidden;
}

div.page {
	position:relative;
	background-color:white;
	margin:16px auto;
	box-shadow: 0px 4px 16px 0px rgba(0,0,0,0.2);
}

div.page img {
	position:absolute;
	user-select:none;
}

div.links { position:absolute; }
div.links a { position:absolute; }
div.links a:hover { outline: 1px dotted blue; }

div.text { position:absolute; }
div.text span {
	position:absolute;
	white-space:pre;
	line-height:1;
	color:transparent;
}

div.searchHitList { position:absolute; }
div.searchHit { position:absolute; pointer-events:none; outline: 1px solid hotpink; background-color: lightpink; mix-blend-mode: multiply; }

div.error {
	padding: 1em;
	color: hotpink;
	font-size: 20pt;
}

</style>
</head>

<body>

<div id="grid-menubar">
	<div class="menu">
		<div class="menu-button">File</div>
		<div class="menu-popup">
			<div onclick="document.getElementById('open-file-input').click()">Open File...</div>
		</div>
	</div>
	<div class="menu">
		<div class="menu-button">Edit</div>
		<div class="menu-popup">
			<div onclick="showSearch()">Search...</div>
		</div>
	</div>
	<div class="menu">
		<div class="menu-button">View</div>
		<div class="menu-popup">
			<div onclick="toggleFullscreen()">Fullscreen</div>
			<div onclick="toggleOutline()">Outline</div>
			<hr>
			<div onclick="setZoom(50)">50%</div>
			<div onclick="setZoom(75)">75% (72 dpi)</div>
			<div onclick="setZoom(100)">100% (96 dpi)</div>
			<div onclick="setZoom(125)">125%</div>
			<div onclick="setZoom(150)">150%</div>
			<div onclick="setZoom(200)">200%</div>
		</div>
	</div>
</div>

<div id="grid-sidebar"><ul id="outline"></ul></div>

<div id="grid-pages" class="sidebarHidden">
	<div id="pages">
		<center style="color:silver;padding-top:3em;">
		<h1>Loading WASM, please wait...</h1>
		</center>
	</div>
</div>

<div id="searchDialog" class="dialog" style="display:none">
<input id="searchText" type="text" size="40" oninput="updateSearch()" placeholder="Search...">
<button onclick="runSearch(-1)">&#x25b2;</button>
<button onclick="runSearch(1)">&#x25bc;</button>
<button onclick="hideSearch()">&#x1f5d9;</button>
<div id="searchStatus">-</div>
</div>

<input type="file" id="open-file-input" style="display:none"
	accept=".pdf,.xps,application/pdf"
	onchange="openFile(event.target.files[0])">

</body>

<script src="mupdf-async.js"></script>
<script>
"use strict";

let doc, pageCount;
let currentPage = 0;
let dirty = [];
let pageDIV = [];
let pageHIT = [];
let zoom = 100;
let dpi = 96;

let searchNeedle = null;
let searchDirty = [];

function toggleFullscreen() {
	if (document.fullscreen)
		document.exitFullscreen();
	else
		document.documentElement.requestFullscreen();
}

function showOutline() {
	document.getElementById("grid-sidebar").style.display = "block";
	document.getElementById("grid-pages").classList.replace("sidebarHidden", "sidebarVisible");
}

function hideOutline() {
	document.getElementById("grid-sidebar").style.display = "none";
	document.getElementById("grid-pages").classList.replace("sidebarVisible", "sidebarHidden");
}

function toggleOutline() {
	let node = document.getElementById("grid-sidebar");
	if (node.style.display === "none" || node.style.display === "")
		showOutline();
	else
		hideOutline();
}

function clearSearch() {
	for (let i = 0; i < pageCount; ++i) {
		if (pageHIT[i])
			emptyNode(pageHIT[i]);
		searchDirty[i] = false;
	}
}

function updateSearch() {
	let searchStatus = document.getElementById("searchStatus");
	searchStatus.textContent = "";
	let newNeedle = document.getElementById("searchText").value;
	if (searchNeedle !== newNeedle) {
		searchNeedle = newNeedle;
		clearSearch();
		if (searchNeedle && searchNeedle.length > 0)
			for (let i = 1; i <= pageCount; ++i)
				searchDirty[i] = true;
		if (searchNeedle && searchNeedle.length > 0)
			updateView();
	}
}

function showSearch() {
	let node = document.getElementById("searchDialog");
	if (node.style.display != "block") {
		node.style.display = "block";
		updateSearch();
	}
	document.getElementById("searchText").focus();
	document.getElementById("searchText").select();
}

function hideSearch() {
	let node = document.getElementById("searchDialog");
	node.style.display = "none";
	searchNeedle = null;
	clearSearch();
}

function runSearchBG(page, dir) {
	let searchStatus = document.getElementById("searchStatus");
	if (searchNeedle && searchNeedle.length > 0) {
		page = page + dir;
		if (page < 1 || page > pageCount) {
			console.log("No more search hits.");
			searchStatus.textContent = "No more search hits.";
		} else {
			console.log("Searching page", page);
			searchStatus.textContent = "Searching page " + page + ".";
			mupdf.search(doc, page, dpi, searchNeedle)
				.then(hits => {
					if (hits.length > 0) {
						pageDIV[page].scrollIntoView();
					} else
						runSearchBG(page, dir);
				});
		}
	} else {
		searchStatus.textContent = "";
	}
}

function runSearch(direction) {
	updateSearch();
	runSearchBG(currentPage, direction);
}

function pageSearch(pageNumber) {
	console.log("SEARCH", pageNumber, JSON.stringify(searchNeedle));
	searchDirty[pageNumber] = false;
	mupdf.search(doc, pageNumber, dpi, searchNeedle)
		.then(hits =>  {
			emptyNode(pageHIT[pageNumber]);
			for (let bbox of hits) {
				let div = document.createElement("div");
				div.classList.add("searchHit");
				div.style.left = bbox.x + 'px';
				div.style.top = bbox.y + 'px';
				div.style.width = bbox.w + 'px';
				div.style.height = bbox.h + 'px';
				pageHIT[pageNumber].appendChild(div);
			}
		});
}

function isVisible(element, slop) {
	let rect = element.getBoundingClientRect();
	if (rect.top >= -slop && rect.bottom <= window.innerHeight + slop)
		return true;
	if (rect.top < window.innerHeight + slop && rect.bottom >= -slop)
		return true;
	return false;
}

function emptyNode(node) {
	while (node.firstChild)
		node.removeChild(node.firstChild);
}

function logError(where, error) {
	console.log("mupdf." + where + ": " + error.name + ": " + error.message);
}

function showDocumentError(where, error) {
	logError(where, error);
	let div = document.createElement("div");
	div.classList.add("error");
	div.textContent = error.name + ": " + error.message;
	emptyNode(document.getElementById("pages"));
	document.getElementById("pages").appendChild(div);
}

function showPageError(where, page, error) {
	logError(where, error);
	let div = document.createElement("div");
	div.classList.add("error");
	div.textContent = error.name + ": " + error.message;
	emptyNode(page);
	page.appendChild(div);
}

async function openURL(url) {
	freeDocument();
	try {
		let response = await fetch(url);
		if (!response.ok)
			throw new Error("Could not fetch document.");
		await initDocument(response, url);
	} catch (error) {
		showDocumentError("initDocument", error);
	}
}

function openFile(file) {
	freeDocument();
	if (file instanceof File) {
		initDocument(file, file.name)
			.catch(error => showDocumentError("initDocument", error));
	}
}

function freeDocument() {
	if (doc) {
		mupdf.freeDocument(doc);
		doc = 0;
		pageCount = 0;
		pageDIV = [];
		dirty = [];
		searchDirty = [];
	}
	emptyNode(document.getElementById("pages"));
	emptyNode(document.getElementById("outline"));
}

async function initDocument(blob, magic) {
	let data = await blob.arrayBuffer();
	doc = await mupdf.openDocument(data, magic);
	pageCount = await mupdf.countPages(doc);
	let title = await mupdf.documentTitle(doc);

	console.log("mupdf: Loaded", JSON.stringify(magic), "with", pageCount, "pages.");

	if (title)
		document.title = title;
	else
		document.title = magic;

	// Use second page as default page size (the cover page is often differently sized)
	let defaultW = await mupdf.pageWidth(doc, pageCount > 1 ? 2 : 1, dpi);
	let defaultH = await mupdf.pageHeight(doc, pageCount > 1 ? 2 : 1, dpi);

	let pagesDiv = document.getElementById("pages");
	pagesDiv.scrollTo(0, 0);
	for (let i = 1; i <= pageCount; ++i) {
		let a = document.createElement("a");
		a.classList.add("anchor");
		a.id = "page" + i;
		pagesDiv.appendChild(a);

		let div = pageDIV[i] = document.createElement("div");
		div.classList.add("page");
		div.style.width = defaultW + 'px';
		div.style.height = defaultH + 'px';
		pagesDiv.appendChild(div);

		dirty[i] = true;
	}

	let outline = await mupdf.documentOutline(doc);
	if (outline) {
		let outlineNode = document.getElementById("outline");
		buildOutline(outlineNode, outline);
		showOutline();
	} else {
		hideOutline();
	}

	updateView();
}

function buildOutline(listNode, outline) {
	for (let item of outline) {
		let itemNode = document.createElement("li");
		let aNode = document.createElement("a");
		aNode.href = "#page" + item.page;
		aNode.textContent = item.title;
		itemNode.appendChild(aNode);
		listNode.appendChild(itemNode);
		if (item.down) {
			itemNode = document.createElement("ul");
			buildOutline(itemNode, item.down);
			listNode.appendChild(itemNode);
		}
	}
}

let zoomLevels = [ 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200 ];

function zoomIn() {
	let curr = zoomLevels.indexOf(zoom);
	let next = zoomLevels[curr + 1];
	if (next)
		setZoom(next);
}

function zoomOut() {
	let curr = zoomLevels.indexOf(zoom);
	let next = zoomLevels[curr - 1];
	if (next)
		setZoom(next);
}

async function setZoom(newZoom) {
	if (zoom === newZoom)
		return;
	zoom = newZoom;
	dpi = (zoom * 96 / 100) | 0;
	let defaultW = await mupdf.pageWidth(doc, pageCount > 1 ? 2 : 1, dpi);
	let defaultH = await mupdf.pageHeight(doc, pageCount > 1 ? 2 : 1, dpi);
	let current = 0;
	for (let i = 1; i <= pageCount; ++i) {
		if (isVisible(pageDIV[i], -100)) {
			current = i;
			break;
		}
	}
	for (let i = 1; i <= pageCount; ++i) {
		dirty[i] = true;
		searchDirty[i] = true;
		pageDIV[i].style.width = defaultW + 'px';
		pageDIV[i].style.height = defaultH + 'px';
		emptyNode(pageDIV[i]);
		pageHIT[i] = null;
	}
	if (current)
		pageDIV[current].scrollIntoView();
	updateView();
}

function parseStructuredText(output, data) {
	let nodes = [];
	let pdf_w = [];
	let html_w = [];
	let text_len = [];
	for (let block of data.blocks) {
		if (block.type === 'text') {
			for (let line of block.lines) {
				let text = document.createElement("span");
				text.style.left = line.bbox.x + 'px';
				text.style.top = (line.y - line.font.size * 0.8) + 'px';
				text.style.height = line.bbox.h + 'px';
				text.style.fontSize = line.font.size + 'px';
				text.style.fontFamily = line.font.family;
				text.style.fontWeight = line.font.weight;
				text.style.fontStyle = line.font.style;
				text.textContent = line.text;
				output.appendChild(text);
				nodes.push(text);
				pdf_w.push(line.bbox.w);
				text_len.push(line.text.length-1);
			}
		}
	}
	for (let i = 0; i < nodes.length; ++i)
		if (text_len[i] > 0)
			html_w[i] = nodes[i].clientWidth;
	for (let i = 0; i < nodes.length; ++i)
		if (text_len[i] > 0)
			nodes[i].style.letterSpacing = ((pdf_w[i] - html_w[i]) / text_len[i]) + 'px';
}

function updateView() {
	let i, n = dirty.length;
	let current = 0;
	for (i = 1; i <= n; ++i) {
		if (!current && isVisible(pageDIV[i], -100))
			current = i;

		if (dirty[i] && isVisible(pageDIV[i], 1000)) {
			console.log("mupdf: drawing page", i);
			let pageNumber = i;
			dirty[pageNumber] = false;
			let div = pageDIV[pageNumber];
			emptyNode(div);

			let img = new Image();
			img.draggable = false;
			// user-select:none disables image.draggable, and we want
			// to keep pointer-events for the link image-map
			img.ondragstart = function () { return false; };
			img.onload = function () {
				URL.revokeObjectURL(this.src);
				div.style.width = this.width + 'px';
				div.style.height = this.height + 'px';
			};
			div.appendChild(img);

			let txt = document.createElement("div");
			txt.classList.add("text");
			div.appendChild(txt);

			let map = document.createElement("div");
			map.classList.add("links");
			div.appendChild(map);

			let hit = pageHIT[i] = document.createElement("div");
			hit.classList.add("searchHitList");
			div.appendChild(hit);

			mupdf.drawPageAsPNG(doc, pageNumber, dpi)
				.then(data => img.src = URL.createObjectURL(new Blob([data], {type:"image/png"})))
				.catch(error => showPageError("drawPageAsPNG", div, error));

			mupdf.pageLinks(doc, pageNumber, dpi)
				.then(data => {
					for (let link of data) {
						let a = document.createElement("a");
						a.href = link.href;
						a.style.left = link.x + 'px';
						a.style.top = link.y + 'px';
						a.style.width = link.w + 'px';
						a.style.height = link.h + 'px';
						map.appendChild(a);
					}
				})
				.catch(error => logError("pageLinks", error));

			mupdf.pageText(doc, pageNumber, dpi)
				.then(data => parseStructuredText(txt, data))
				.catch(error => logError("pageText", error));
		}

		if (searchNeedle && searchNeedle.length > 0) {
			if (searchDirty[i] && isVisible(pageDIV[i], 0))
				pageSearch(i);
		}
	}
	if (current)
		currentPage = current;
}

/* Wait 50ms until scrolling has stopped before sending off page draw requests */
let scrollTimer = null;
document.addEventListener("scroll", function (event) {
	if (scrollTimer !== null)
		clearTimeout(scrollTimer);
	scrollTimer = setTimeout(function () {
		scrollTimer = null;
		updateView();
	}, 50);
})

let zoomTimer = null;
window.addEventListener("wheel", function (event) {
	if (event.ctrlKey || event.metaKey) {
		event.preventDefault();
		if (zoomTimer)
			return;
		zoomTimer = setTimeout(function () { zoomTimer = null; }, 250);
		if (event.deltaY < 0)
			zoomIn();
		else if (event.deltaY > 0)
			zoomOut();
	}
}, {passive: false});

window.addEventListener("keydown", function (event) {
	if (event.ctrlKey || event.metaKey) {
		switch (event.keyCode) {
		// '=' / '+' on various keyboards
		case 61:
		case 107:
		case 187:
		case 171:
			zoomIn();
			event.preventDefault();
			break;
		// '-'
		case 173:
		case 109:
		case 189:
			zoomOut();
			event.preventDefault();
			break;
		// '0'
		case 48:
		case 96:
			setZoom(100);
			break;
		case 70: // 'F':
			event.preventDefault();
			showSearch();
			break;
		case 71: // 'G':
			event.preventDefault();
			showSearch();
			runSearch(event.shiftKey ? -1 : 1);
			break;
		}
	}
});

window.onerror = function (message, source, line, col, error) {
	alert(message);
}

mupdf.oninit = function () {
	emptyNode(document.getElementById("pages"));
	let params = new URLSearchParams(window.location.search);
	if (params.has("file"))
		openURL(params.get("file"));
}

</script>
</html>
